﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Caching;

using Fuse8.DomainFramework.Domain;
using Fuse8.DomainFramework.Common;

namespace Fuse8.DomainFramework.CacheService
{
    /// <summary>
    /// 
    /// </summary>
    /// <typeparam name="TEntity"></typeparam>
    /// <typeparam name="TKey"></typeparam>
    public abstract class BaseDependencyCacheService<TEntity, TKey> : DefaultCacheService<TEntity,TKey>
        where TEntity : class, IEntityObject<TKey>, new()
        where TKey : IComparable<TKey>, IEquatable<TKey> 
    {
        #region Fields

        private static readonly object _syncRoot = new object();
		private static readonly ICacheKeyService _cacheKeyService = new CacheKeyFacade();
        
        /// <summary>
        /// 
        /// </summary>
        protected static readonly string DependencyCacheKey = "{1}_dependencyList_{0}";

        #endregion

        #region Override base methods

        /// <summary>
        /// Store dependencies in concrete cache
        /// </summary>
        /// <param name="cacheKey"></param>
        public override void AddToDependency(string cacheKey)
        {
            base.AddToDependency(cacheKey);

            lock (_syncRoot)
            {
            	var dependencyKey = _cacheKeyService.GenerateDependencyCacheKey(new TEntity());

                var cacheItem = ClientGet<EntityCacheItem<TEntity>>(dependencyKey);
                if (cacheItem != null)
                {
                    if (cacheItem.Dependency == null)
                    {
                        cacheItem.Dependency = new List<EntityCacheItemDependency>();
                    }

                    cacheItem.Dependency.Add(new EntityCacheItemDependency {Tag = cacheKey});
                }
                else
                {
                    var list = new List<EntityCacheItemDependency> {new EntityCacheItemDependency {Tag = cacheKey}};
                	cacheItem = new EntityCacheItem<TEntity>
                    {
                        Dependency = list
                    };
                }

                InternalAddCacheItemToCache(dependencyKey, cacheItem);
            }
        }

        /// <summary>
        /// Clear dependencies from concrete cache
        /// </summary>
        public override void ClearDependency(TEntity entityObject)
        {
            base.ClearDependency(entityObject);

            InternalClearDependency();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="id"></param>
        public override void ClearDependency(TKey id)
        {
            base.ClearDependency(id);

            InternalClearDependency();
        }

        #endregion

        #region DefaultCacheService overrides

        /// <summary>
        /// 
        /// </summary>
        /// <param name="cacheKey"></param>
        /// <param name="entityObject"></param>
        protected override void DirectAddItemToCache(string cacheKey, TEntity entityObject)
        {
            Guard.ArgumentNotEmpty(cacheKey, "cacheKey");
            Guard.ArgumentNotNull(entityObject, "entityObject");

            lock (_syncRoot)
            {
                Invalidate(new[] { cacheKey });

                var list = new List<TEntity> {entityObject};

            	var dependencyKeys = new[] { _cacheKeyService.GenerateCacheKey(entityObject) }.Concat(_cacheKeyService.GenerateCacheKeysForLinkedDependency(entityObject));                

                var cacheItem = new EntityCacheItem<TEntity>
                {
                    Key = cacheKey,
                    Entity = list,
                    Dependency = CreateDependency(cacheKey, dependencyKeys)
                };

                InternalAddCacheItemToCache(cacheKey, cacheItem);                
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="cacheKey"></param>
        /// <param name="entityObjects"></param>
        protected override void DirectAddListToCache(string cacheKey, IList<TEntity> entityObjects)
        {
            Guard.ArgumentNotEmpty(cacheKey, "cacheKey");
            Guard.ArgumentNotNull(entityObjects, "entityObjects");

            lock (_syncRoot)
            {
                Invalidate(new[] { cacheKey });
				var dependencyKeys = entityObjects.Select(p => _cacheKeyService.GenerateCacheKey(p)).Concat(entityObjects.SelectMany(p => _cacheKeyService.GenerateCacheKeysForLinkedDependency(p)));
                var cacheItem = new EntityCacheItem<TEntity>
                {
                    Key = cacheKey,
                    Entity = entityObjects,
                    Dependency = CreateDependency(cacheKey, dependencyKeys)
                };
                InternalAddCacheItemToCache(cacheKey, cacheItem);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="cacheKey"></param>
        /// <returns></returns>
        protected override TEntity DirectGetFromCache(string cacheKey)
        {
            Guard.ArgumentNotEmpty(cacheKey, "cacheKey");

            TEntity result = null;

            lock (_syncRoot)
            {
                var cacheItem = InternalGet(cacheKey);
                if ((cacheItem != null) && (cacheItem.Count > 0))
                {
                    result = cacheItem[0];
                }
            }

            return result;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="cacheKey"></param>
        /// <returns></returns>
        protected override IList<TEntity> DirectGetListFromCache(string cacheKey)
        {
            Guard.ArgumentNotEmpty(cacheKey, "cacheKey");

            IList<TEntity> result;

            lock (_syncRoot)
            {
                result = InternalGet(cacheKey);
            }

            return result;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="cacheKey"></param>
        protected override void DirectRemoveItemFromCache(string cacheKey)
        {
            Guard.ArgumentNotEmpty(cacheKey, "cacheKey");

            lock (_syncRoot)
            {
                InternalRemoveCacheItemFromCache<EntityCacheItem<TEntity>>(cacheKey);

                Invalidate(new[] { cacheKey });
            }
        }

        /// <summary>
        /// 
        /// </summary>
        protected override void DirectClearCache()
        {
            lock (_syncRoot)
            {
                ClientFlushAll();
            }
        }

        #endregion

        #region Private and protected methods

        private void InternalClearDependency()
        {
            lock (_syncRoot)
            {
            	var dependencyKey = _cacheKeyService.GenerateDependencyCacheKey(new TEntity());
                var cacheItem = ClientGet<EntityCacheItem<TEntity>>(dependencyKey);

                if ((cacheItem != null) && (cacheItem.Dependency != null))
                {
                    var keys = cacheItem.Dependency.Select(p => p.Tag).ToArray();

                    foreach (var key in keys)
                    {
                        InternalRemoveCacheItemFromCache<EntityCacheItem<TEntity>>(key);
                    }

                    InternalRemoveCacheItemFromCache<string>(dependencyKey);
                }
            }
        }
        
        private IList<EntityCacheItemDependency> CreateDependency(string cacheKey, IEnumerable<string> tags)
        {
            IList<EntityCacheItemDependency> result = null;

            if (tags != null)
            {
                result = new List<EntityCacheItemDependency>();

            	tags = tags.Distinct();

				foreach (var tag in tags.Where(p => !p.Equals(cacheKey, StringComparison.OrdinalIgnoreCase)))
                {
                    var cacheItemDependency = new EntityCacheItemDependency
                                                                        {
                                                                        	Tag = cacheKey,
                                                                        	TagVersion = DateTime.UtcNow.Ticks
                                                                        };

					InternalAddDependencyTagToCache(_cacheKeyService.GenerateDependencyCacheKeyTag(tag), cacheItemDependency);

                    result.Add(cacheItemDependency);
                }
            }

            return (result != null &&  result.Count > 0) ? result : null;
        }

        private void Invalidate(IEnumerable<string> tags)
        {
            if (tags != null)
            {
				var items = ClientGet<EntityCacheItemDependency>(tags.Select(p => _cacheKeyService.GenerateDependencyCacheKeyTag(p)));

                foreach (var item in items)
                {
                    ClientRemove<EntityCacheItem<TEntity>>(item.Value.Tag);
                }
            }
        } 

        private IList<TEntity> InternalGet(string cacheKey)
        {
            Guard.ArgumentNotEmpty(cacheKey, "cacheKey");

            IList<TEntity> result = null;

            var cacheItem = ClientGet<EntityCacheItem<TEntity>>(cacheKey);
            if (cacheItem != null)
            {
                if ((cacheItem.Entity != null) && (cacheItem.Entity.Count > 0))
                {
                    result = cacheItem.Entity;
                }
            }

            return result;
        }

        private void InternalAddCacheItemToCache(string cacheKey, EntityCacheItem<TEntity> cacheItem)
        {
            Guard.ArgumentNotEmpty(cacheKey, "cacheKey");
            Guard.ArgumentNotNull(cacheItem, "entityObject");

            AsyncAddToCache caller = (mode, key, item) => 
                                     	{
                                     		var tagAddKey = _cacheKeyService.GenerateAddCacheKyeTag(cacheKey);
                                     		if (ClientStore(CacheStoreMode.Add, tagAddKey, LockedOnAddValue, DateTime.Now.Add(CacheOptions.TagAddExpiration), Cache.NoSlidingExpiration))
                                     		{
                                     			ClientStore(mode, cacheKey, cacheItem, CacheOptions.AbsoluteExpiration, CacheOptions.SlidingExpiration);
                                     			ClientRemove<string>(tagAddKey);
                                     		}
                                     		return true;
                                     	};
            
            if (CacheOptions.UseAsyncMethods)
            {
                caller.BeginInvoke(CacheStoreMode.Set, cacheKey, cacheItem, InternalAddCacheItemToCacheCallback, null);
            }
            else
            {
                caller.Invoke(CacheStoreMode.Set, cacheKey, cacheItem);
            }
        }

        private static void InternalAddCacheItemToCacheCallback(IAsyncResult result)
        {
            Guard.ArgumentNotNull(result, "result");

            var caller = result.AsyncState as AsyncAddToCache;

            if (caller != null)
            {
                caller.EndInvoke(result);
            }
        }

        private void InternalAddDependencyTagToCache(string cacheKey, EntityCacheItemDependency dependency)
        {
            Guard.ArgumentNotEmpty(cacheKey, "cacheKey");

            AsyncAddDependencyToCache caller = ClientStore;

            if (CacheOptions.UseAsyncMethods)
            {
                caller.BeginInvoke(CacheStoreMode.Set, cacheKey, dependency, CacheOptions.AbsoluteExpiration, CacheOptions.TagSlidingExpiration, InternalAddDependencyTagToCacheCallback, null);
            }
            else
            {
                caller.Invoke(CacheStoreMode.Set, cacheKey, dependency, CacheOptions.AbsoluteExpiration, CacheOptions.TagSlidingExpiration);
            }
        }

        private static void InternalAddDependencyTagToCacheCallback(IAsyncResult result)
        {
            Guard.ArgumentNotNull(result, "result");

            var caller = result.AsyncState as AsyncAddDependencyToCache;

            if (caller != null)
            {
                caller.EndInvoke(result);
            }
        }

        private void InternalRemoveCacheItemFromCache<TStored>(string cacheKey)
        {
            Guard.ArgumentNotEmpty(cacheKey, "cacheKey");

            var caller = new AsyncRemoveFromCache(ClientRemove<TStored>);

            if (CacheOptions.UseAsyncMethods)
            {
                caller.BeginInvoke(cacheKey, InternalRemoveCacheItemFromCacheCallback, null);
            }
            else
            {
                caller.Invoke(cacheKey);
            }
        }

        private static void InternalRemoveCacheItemFromCacheCallback(IAsyncResult result)
        {
            Guard.ArgumentNotNull(result, "result");

            var caller = result.AsyncState as AsyncRemoveFromCache;

            if (caller != null)
            {
                caller.EndInvoke(result);
            }
        }

        #endregion

        #region Abstract methods

        /// <summary>
        /// 
        /// </summary>
        /// <param name="cacheKey"></param>
        /// <returns></returns>
        protected abstract TStored ClientGet<TStored>(string cacheKey);

        /// <summary>
        /// 
        /// </summary>
        /// <param name="cacheKey"></param>
        /// <returns></returns>
        protected abstract IDictionary<string, TStored> ClientGet<TStored>(IEnumerable<string> cacheKey);

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TStored"></typeparam>
        /// <param name="mode"></param>
        /// <param name="cacheKey"></param>
        /// <param name="cachedObject"></param>
        /// <param name="absoluteExpiration"></param>
        /// <param name="slidingExpiration"></param>
        /// <returns></returns>
        protected abstract bool ClientStore<TStored>(CacheStoreMode mode, string cacheKey, TStored cachedObject, DateTime absoluteExpiration, TimeSpan slidingExpiration);

        /// <summary>
        /// 
        /// </summary>
        /// <param name="cacheKey"></param>
        /// <returns></returns>
        protected abstract bool ClientRemove<TStored>(string cacheKey);

        /// <summary>
        /// 
        /// </summary>
        protected abstract void ClientFlushAll();

        #endregion
    }
}
